Schritt-für-Schritt Entwicklung einer Movie API
APIView • Generic Views • ViewSets • Best Practices
Überblick über DRF Views
Hierarchie verstehen
Die Basis aller Views
Manuelle Kontrolle
CRUD von Hand
Vorgefertigte Patterns
Weniger Code
Mixins nutzen
Alle CRUD-Operationen
Router-Integration
Custom Actions
Von maximaler Kontrolle zu maximaler Produktivität
Maximale Kontrolle, maximaler Code
class MovieListAPIView(APIView):
def get(self, request):
# Alles manuell
movies = Movie.objects.all()
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data)
Vorgefertigte Patterns, weniger Code
class MovieListView(generics.ListCreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET + POST automatisch!
Alle CRUD-Operationen in einer Klasse
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET (list/detail), POST, PUT, PATCH, DELETE!
Entscheidungshilfe:
Django-Projekt mit Movie Models bereits erstellt
# Bereits ausgeführt:
django-admin startproject movieapi .
python manage.py startapp movies
# Projektstruktur:
movieapi/
├── movieapi/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── movies/
│ ├── __init__.py
│ ├── models.py # ← Models vorhanden (Movie, Artist, MovieCasting)
│ ├── views.py # ← Hier arbeiten wir!
│ ├── urls.py # ← Werden wir erstellen
│ └── admin.py
└── manage.py
# movies/models.py
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.IntegerField()
genre = models.CharField(max_length=100, blank=True)
rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Artist(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
birth_date = models.DateField(null=True, blank=True)
# ...
class MovieCasting(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings')
artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='movie_roles')
role_name = models.CharField(max_length=200)
# ...
Wir entwickeln die Views in 3 verschiedenen Ansätzen:
# Terminal:
pip install djangorestframework
# Output:
Successfully installed djangorestframework-3.14.0
# filepath: movieapi/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party
'rest_framework', # ← NEU!
# Local apps
'movies', # ← Bereits vorhanden
]
# Am Ende der Datei hinzufügen:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny', # Für Development
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
}
# Terminal:
python manage.py makemigrations
python manage.py migrate
# Output:
Migrations for 'movies':
movies/migrations/0001_initial.py
- Create model Movie
- Create model Artist
- Create model MovieCasting
Operations to perform:
Apply all migrations: movies
Running migrations:
Applying movies.0001_initial... OK
Konvertieren Models zu JSON (und zurück)
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting
class MovieSerializer(serializers.ModelSerializer):
"""Serializer für Movie Model"""
class Meta:
model = Movie
fields = '__all__' # Alle Felder
read_only_fields = ['created_at', 'updated_at']
class ArtistSerializer(serializers.ModelSerializer):
"""Serializer für Artist Model"""
full_name = serializers.ReadOnlyField() # Property aus Model
class Meta:
model = Artist
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
class MovieCastingSerializer(serializers.ModelSerializer):
"""Serializer für MovieCasting Model"""
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['created_at']
class MovieCastingDetailSerializer(serializers.ModelSerializer):
"""Detaillierter Casting Serializer mit verschachtelten Objekten"""
movie = MovieSerializer(read_only=True)
artist = ArtistSerializer(read_only=True)
class Meta:
model = MovieCasting
fields = '__all__'
Alle HTTP-Methoden manuell implementieren
# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Movie
from .serializers import MovieSerializer
class MovieListAPIView(APIView):
"""
GET: Liste aller Filme
"""
def get(self, request):
"""Alle Filme abrufen"""
# 1. Daten aus DB holen
movies = Movie.objects.all()
# 2. Serialisieren (Python → JSON)
serializer = MovieSerializer(movies, many=True)
# 3. Response zurückgeben
return Response(serializer.data, status=status.HTTP_200_OK)
# filepath: movies/urls.py
from django.urls import path
from .views import MovieListAPIView
urlpatterns = [
path('movies/', MovieListAPIView.as_view(), name='movie-list'),
]
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('movies.urls')), # ← NEU!
]
python manage.py runserver
# Browser: http://127.0.0.1:8000/api/movies/
# Output: [] (leere Liste, da noch keine Filme)
# filepath: movies/views.py
class MovieListAPIView(APIView):
"""
GET: Liste aller Filme
POST: Film erstellen
"""
def get(self, request):
"""Alle Filme abrufen"""
movies = Movie.objects.all()
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request):
"""Film erstellen"""
# 1. Daten aus Request holen & validieren
serializer = MovieSerializer(data=request.data)
# 2. Validierung
if serializer.is_valid():
# 3. Speichern
serializer.save()
# 4. Response zurückgeben
return Response(serializer.data, status=status.HTTP_201_CREATED)
# Fehler zurückgeben
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
http://127.0.0.1:8000/api/movies/{
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"description": "A computer hacker..."
}
# filepath: movies/views.py
from django.shortcuts import get_object_or_404
class MovieDetailAPIView(APIView):
"""
GET: Film-Details
PUT: Film komplett aktualisieren
PATCH: Film teilweise aktualisieren
DELETE: Film löschen
"""
def get_object(self, pk):
"""Helper: Film holen oder 404"""
return get_object_or_404(Movie, pk=pk)
def get(self, request, pk):
"""Film-Details abrufen"""
movie = self.get_object(pk)
serializer = MovieSerializer(movie)
return Response(serializer.data, status=status.HTTP_200_OK)
def put(self, request, pk):
"""Film komplett aktualisieren (alle Felder)"""
movie = self.get_object(pk)
serializer = MovieSerializer(movie, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, pk):
"""Film teilweise aktualisieren (nur geänderte Felder)"""
movie = self.get_object(pk)
serializer = MovieSerializer(movie, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
"""Film löschen"""
movie = self.get_object(pk)
movie.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# filepath: movies/urls.py
from .views import MovieListAPIView, MovieDetailAPIView
urlpatterns = [
path('movies/', MovieListAPIView.as_view(), name='movie-list'),
path('movies//', MovieDetailAPIView.as_view(), name='movie-detail'), # ← NEU
]
GET /api/movies/ # Liste
POST /api/movies/ # Erstellen
GET /api/movies/1/ # Details
PUT /api/movies/1/ # Update (komplett)
PATCH /api/movies/1/ # Update (teilweise)
DELETE /api/movies/1/ # Löschen
Vorgefertigte Patterns für Standard-CRUD → Weniger Code, gleiche Funktionalität
DRF bietet Klassen für Standard-CRUD-Operationen
DRF stellt fertige View-Klassen bereit:
Die Bausteine hinter Generic Views:
ListModelMixinCreateModelMixinRetrieveModelMixinUpdateModelMixinDestroyModelMixinKönnen selbst kombiniert werden!
# filepath: movies/views.py
from rest_framework import generics
from .models import Movie
from .serializers import MovieSerializer
class MovieListCreateView(generics.ListCreateAPIView):
"""
GET: Liste aller Filme
POST: Film erstellen
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Fertig! GET + POST funktionieren automatisch!
class MovieRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
"""
GET: Film-Details
PUT: Film komplett aktualisieren
PATCH: Film teilweise aktualisieren
DELETE: Film löschen
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Fertig! GET + PUT + PATCH + DELETE automatisch!
# filepath: movies/urls.py
from django.urls import path
from .views import MovieListCreateView, MovieRetrieveUpdateDestroyView
urlpatterns = [
path('movies/', MovieListCreateView.as_view(), name='movie-list'),
path('movies//', MovieRetrieveUpdateDestroyView.as_view(), name='movie-detail'),
]
~70 Zeilen Code
~10 Zeilen Code
Überschreibe Methoden für Custom-Logik
class MovieListCreateView(generics.ListCreateAPIView):
serializer_class = MovieSerializer
def get_queryset(self):
"""Custom QuerySet: Nur Filme ab 2000"""
queryset = Movie.objects.all()
# Query-Parameter auslesen
year = self.request.query_params.get('year')
if year:
queryset = queryset.filter(year__gte=year)
return queryset
# Verwendung: GET /api/movies/?year=2010
class MovieListCreateView(generics.ListCreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def perform_create(self, serializer):
"""Überschreibe Speicher-Logik"""
# Beispiel: User aus Request hinzufügen
# serializer.save(created_by=self.request.user)
# Oder Custom-Validierung
if serializer.validated_data['year'] < 1888:
raise ValidationError("Jahr zu früh!")
serializer.save()
from rest_framework.response import Response
class MovieRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def destroy(self, request, *args, **kwargs):
"""Custom DELETE mit zusätzlicher Info"""
instance = self.get_object()
title = instance.title
self.perform_destroy(instance)
return Response({
'message': f'Film "{title}" wurde gelöscht',
'success': True
}, status=status.HTTP_200_OK)
Nur GET Liste
class MovieListView(generics.ListAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET /api/movies/ ✅
Nur POST
class MovieCreateView(generics.CreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# POST /api/movies/ ✅
Nur GET Detail
class MovieDetailView(generics.RetrieveAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET /api/movies/1/ ✅
Nur PUT/PATCH
class MovieUpdateView(generics.UpdateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# PUT /api/movies/1/ ✅
# PATCH /api/movies/1/ ✅
Nur DELETE
class MovieDeleteView(generics.DestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# DELETE /api/movies/1/ ✅
GET + POST
class MovieListCreateView(generics.ListCreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET /api/movies/ ✅
# POST /api/movies/ ✅
GET + PUT + PATCH
class MovieRUView(generics.RetrieveUpdateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET /api/movies/1/ ✅
# PUT /api/movies/1/ ✅
# PATCH /api/movies/1/ ✅
GET + PUT + PATCH + DELETE
class MovieRUDView(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# GET /api/movies/1/ ✅
# PUT /api/movies/1/ ✅
# PATCH /api/movies/1/ ✅
# DELETE /api/movies/1/ ✅
Generic Views sind Kombinationen aus Mixins + GenericAPIView
from rest_framework import mixins
# 1. ListModelMixin - GET Liste
# 2. CreateModelMixin - POST
# 3. RetrieveModelMixin - GET Detail
# 4. UpdateModelMixin - PUT/PATCH
# 5. DestroyModelMixin - DELETE
from rest_framework import mixins, generics
class MovieCustomView(mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
Custom: Nur GET Liste, POST und DELETE
(kein PUT/PATCH)
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def get(self, request, *args, **kwargs):
if 'pk' in kwargs:
# DELETE braucht get_object, aber wir haben kein RetrieveMixin
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
Generic Views = Mixins + GenericAPIView
Du kannst Mixins selbst kombinieren für Custom-Kombinationen!
Minimaler Code, maximale Funktionalität
# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie, Artist, MovieCasting
from .serializers import MovieSerializer, ArtistSerializer, MovieCastingSerializer
class MovieViewSet(viewsets.ModelViewSet):
"""
ViewSet für Movie Model
Automatisch verfügbar:
- list (GET /api/movies/)
- create (POST /api/movies/)
- retrieve (GET /api/movies/{id}/)
- update (PUT /api/movies/{id}/)
- partial_update (PATCH /api/movies/{id}/)
- destroy (DELETE /api/movies/{id}/)
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Fertig! Alle CRUD-Operationen funktionieren!
class ArtistViewSet(viewsets.ModelViewSet):
"""ViewSet für Artist Model"""
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
class MovieCastingViewSet(viewsets.ModelViewSet):
"""ViewSet für MovieCasting Model"""
queryset = MovieCasting.objects.all()
serializer_class = MovieCastingSerializer
# filepath: movies/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet
# Router erstellen
router = DefaultRouter()
# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')
router.register(r'castings', MovieCastingViewSet, basename='casting')
# URLs
urlpatterns = [
path('', include(router.urls)),
]
GET /api/movies/ # Liste
POST /api/movies/ # Erstellen
GET /api/movies/1/ # Details
PUT /api/movies/1/ # Update (komplett)
PATCH /api/movies/1/ # Update (teilweise)
DELETE /api/movies/1/ # Löschen
# Gleiche für artists/ und castings/
ViewSets können mit @action erweitert werden
# filepath: movies/views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False, methods=['get'])
def top_rated(self, request):
"""
Custom Endpoint: GET /api/movies/top_rated/
Top 10 bestbewertete Filme
"""
movies = Movie.objects.filter(
rating__isnull=False
).order_by('-rating')[:10]
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""
Custom Endpoint: GET /api/movies/recent/
Filme der letzten 5 Jahre
"""
from datetime import datetime
current_year = datetime.now().year
movies = Movie.objects.filter(year__gte=current_year - 5)
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
"""
Custom Endpoint: GET /api/movies/{id}/castings/
Besetzung eines Films
"""
movie = self.get_object()
castings = movie.castings.all()
serializer = MovieCastingSerializer(castings, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def add_to_favorites(self, request, pk=None):
"""
Custom Endpoint: POST /api/movies/{id}/add_to_favorites/
Film zu Favoriten hinzufügen
"""
movie = self.get_object()
# Hier könnte Favoriten-Logik sein
# request.user.favorites.add(movie)
return Response({
'status': 'Film zu Favoriten hinzugefügt',
'movie': movie.title
}, status=status.HTTP_200_OK)
URL ohne ID
@action(detail=False, methods=['get'])
def top_rated(self, request):
# ...
return Response(data)
# URL: /api/movies/top_rated/
# Arbeitet mit ganzer Collection
Verwendung:
URL mit ID
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
movie = self.get_object()
# ...
return Response(data)
# URL: /api/movies/{id}/castings/
# Arbeitet mit einem Objekt
Verwendung:
HTTP-Methode(n)
@action(detail=True, methods=['get'])
def stats(self, request, pk=None):
# GET /api/movies/{id}/stats/
pass
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
# POST /api/movies/{id}/publish/
pass
@action(detail=False, methods=['get', 'post'])
def bulk_import(self, request):
# GET & POST /api/movies/bulk_import/
pass
Custom URL-Namen
@action(
detail=True,
methods=['post'],
url_path='mark-as-favorite', # URL
url_name='mark_favorite' # Reverse-URL
)
def mark_favorite(self, request, pk=None):
# POST /api/movies/{id}/mark-as-favorite/
pass
# Reverse:
# reverse('movie-mark_favorite', args=[1])
Für Read-Only APIs (z.B. öffentliche Daten)
# filepath: movies/views.py
from rest_framework import viewsets
class PublicMovieViewSet(viewsets.ReadOnlyModelViewSet):
"""
Öffentliche Movie-API (nur Lesen)
Verfügbar:
- list (GET /api/public/movies/)
- retrieve (GET /api/public/movies/{id}/)
NICHT verfügbar:
- create (POST) ❌
- update (PUT/PATCH) ❌
- destroy (DELETE) ❌
"""
queryset = Movie.objects.filter(rating__gte=7.0) # Nur gute Filme
serializer_class = MovieSerializer
# filepath: movies/urls.py
from .views import PublicMovieViewSet
public_router = DefaultRouter()
public_router.register(r'public/movies', PublicMovieViewSet, basename='public-movie')
urlpatterns = [
path('', include(router.urls)), # Admin-API
path('', include(public_router.urls)), # Public-API
]
# 2 separate Klassen
class MovieListCreateView(generics.ListCreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
class MovieDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# URLs manuell
urlpatterns = [
path('movies/', MovieListCreateView.as_view()),
path('movies//', MovieDetailView.as_view()),
]
# 1 Klasse für alles
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Router generiert URLs automatisch
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = [
path('', include(router.urls)),
]
# filepath: movies/views.py
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Filter Backends aktivieren
filter_backends = [
DjangoFilterBackend, # Exakte Filter
filters.SearchFilter, # Textsuche
filters.OrderingFilter # Sortierung
]
# Welche Felder können gefiltert werden?
filterset_fields = ['year', 'genre']
# Welche Felder können durchsucht werden?
search_fields = ['title', 'description']
# Welche Felder können sortiert werden?
ordering_fields = ['year', 'rating', 'title']
# Standard-Sortierung
ordering = ['-year'] # Neueste zuerst
# Filtering (exakt):
GET /api/movies/?year=2010
GET /api/movies/?genre=Sci-Fi
GET /api/movies/?year=2010&genre=Action
# Search (enthält):
GET /api/movies/?search=Matrix
GET /api/movies/?search=Inception
# Ordering:
GET /api/movies/?ordering=year # Aufsteigend
GET /api/movies/?ordering=-year # Absteigend
GET /api/movies/?ordering=-rating,title # Mehrfach
# Kombiniert:
GET /api/movies/?year=2010&search=sci&ordering=-rating
pip install django-filter
# settings.py:
INSTALLED_APPS = [
# ...
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
]
}
Verhindert zu große Responses
# filepath: movieapi/settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
# filepath: movies/views.py
from rest_framework.pagination import PageNumberPagination
class MoviePagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
pagination_class = MoviePagination # Custom Pagination
GET /api/movies/?page=2
# Response:
{
"count": 145,
"next": "http://api.example.org/movies/?page=3",
"previous": "http://api.example.org/movies/?page=1",
"results": [
{"id": 11, "title": "Movie 11", ...},
{"id": 12, "title": "Movie 12", ...},
// ... 20 Einträge
]
}
class MovieViewSet(viewsets.ModelViewSet):
serializer_class = MovieSerializer
def get_queryset(self):
"""Nur Filme des aktuellen Users"""
user = self.request.user
if user.is_staff:
# Admins sehen alles
return Movie.objects.all()
else:
# Normale User nur eigene
return Movie.objects.filter(created_by=user)
class MovieViewSet(viewsets.ModelViewSet):
serializer_class = MovieSerializer
def get_queryset(self):
"""Custom Filter via Query-Params"""
queryset = Movie.objects.all()
# ?year_min=2010
year_min = self.request.query_params.get('year_min')
if year_min:
queryset = queryset.filter(year__gte=year_min)
# ?year_max=2020
year_max = self.request.query_params.get('year_max')
if year_max:
queryset = queryset.filter(year__lte=year_max)
# ?has_rating=true
has_rating = self.request.query_params.get('has_rating')
if has_rating == 'true':
queryset = queryset.filter(rating__isnull=False)
return queryset
# Verwendung:
# GET /api/movies/?year_min=2010&year_max=2020&has_rating=true
class MovieViewSet(viewsets.ModelViewSet):
serializer_class = MovieSerializer
def get_queryset(self):
"""Mit select_related für Performance"""
action = self.action
if action == 'list':
# Liste: Weniger Daten
return Movie.objects.all().only('id', 'title', 'year', 'rating')
elif action == 'retrieve':
# Details: Alle Daten + Related
return Movie.objects.all().select_related().prefetch_related('castings')
return Movie.objects.all()
# filepath: movies/serializers.py
class MovieListSerializer(serializers.ModelSerializer):
"""Einfacher Serializer für Listen"""
class Meta:
model = Movie
fields = ['id', 'title', 'year', 'genre', 'rating']
class MovieDetailSerializer(serializers.ModelSerializer):
"""Detaillierter Serializer für Einzelansicht"""
castings = MovieCastingDetailSerializer(many=True, read_only=True)
class Meta:
model = Movie
fields = '__all__'
# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
def get_serializer_class(self):
"""Wähle Serializer basierend auf Action"""
if self.action == 'list':
return MovieListSerializer # Einfach für Listen
return MovieDetailSerializer # Detailliert für Details/Create/Update
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
def get_serializer_class(self):
"""Verschiedene Serializer für Create/Read"""
if self.action in ['create', 'update', 'partial_update']:
return MovieCreateSerializer # Mit Validierung
return MovieSerializer # Mit verschachtelten Daten
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def perform_create(self, serializer):
"""Custom Logik beim Erstellen"""
# User automatisch hinzufügen
serializer.save(created_by=self.request.user)
# Oder: Logging
movie = serializer.save()
logger.info(f"Film erstellt: {movie.title} von {self.request.user}")
# Oder: Zusätzliche Validierung
if serializer.validated_data['year'] < 1888:
raise ValidationError("Film zu alt!")
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def perform_update(self, serializer):
"""Custom Logik beim Update"""
# Alte Werte merken
old_title = serializer.instance.title
# Speichern
movie = serializer.save()
# Logging
if old_title != movie.title:
logger.info(f"Film umbenannt: {old_title} → {movie.title}")
# Notification senden
# send_notification(f"Film {movie.title} wurde aktualisiert")
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def perform_destroy(self, instance):
"""Custom Logik beim Löschen"""
# Prüfung
if instance.castings.count() > 0:
raise ValidationError("Film hat noch Besetzungen!")
# Logging
logger.warning(f"Film gelöscht: {instance.title}")
# Archivieren statt löschen
instance.is_deleted = True
instance.save()
# Oder wirklich löschen:
# instance.delete()
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
permission_classes = [IsAuthenticated] # Nur eingeloggte User
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def get_permissions(self):
"""Verschiedene Permissions pro Action"""
if self.action in ['list', 'retrieve']:
# Lesen für alle
permission_classes = [AllowAny]
elif self.action in ['create', 'update', 'partial_update']:
# Schreiben nur für eingeloggte
permission_classes = [IsAuthenticated]
else:
# Löschen nur für Admins
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
# filepath: movies/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""Nur Owner kann ändern"""
def has_object_permission(self, request, view, obj):
# Lesen für alle
if request.method in permissions.SAFE_METHODS:
return True
# Schreiben nur für Owner
return obj.created_by == request.user
# In ViewSet:
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
permission_classes = [IsOwnerOrReadOnly]
~70 Zeilen Code
class MovieListAPIView(APIView):
def get(self, request):
movies = Movie.objects.all()
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data)
def post(self, request):
serializer = MovieSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
class MovieDetailAPIView(APIView):
def get_object(self, pk):
return get_object_or_404(Movie, pk=pk)
def get(self, request, pk):
movie = self.get_object(pk)
serializer = MovieSerializer(movie)
return Response(serializer.data)
def put(self, request, pk):
movie = self.get_object(pk)
serializer = MovieSerializer(movie, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)
def delete(self, request, pk):
movie = self.get_object(pk)
movie.delete()
return Response(status=204)
# URLs:
urlpatterns = [
path('movies/', MovieListAPIView.as_view()),
path('movies//', MovieDetailAPIView.as_view()),
]
~10 Zeilen Code
from rest_framework import generics
class MovieListCreateView(generics.ListCreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
class MovieDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# URLs:
urlpatterns = [
path('movies/', MovieListCreateView.as_view()),
path('movies//', MovieDetailView.as_view()),
]
~5 Zeilen Code
from rest_framework import viewsets
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# URLs (Router):
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = [
path('', include(router.urls)),
]
GET /api/movies/ ✅
POST /api/movies/ ✅
GET /api/movies/1/ ✅
PUT /api/movies/1/ ✅
PATCH /api/movies/1/ ✅
DELETE /api/movies/1/ ✅
select_related() für ForeignKeysprefetch_related() für Many-to-Many.only() für Listen (nur benötigte Felder)read_only_fields für Timestamps@action für nicht-CRUD-Operationendetail=True für einzelne Objektedetail=False für Collectionstop_rated, recentfilterset_fieldsAllowAny in Produktionget_queryset() überschriebenget_serializer_class() dynamischperform_create/update/destroy()~70 Zeilen Code
Viel Boilerplate
~10 Zeilen Code
Gute Balance
~5 Zeilen Code
Production-Ready!
JWT Tokens implementieren
Login/Logout Endpoints
Granulare Zugriffsrechte
Custom Permissions
API-Tests schreiben
APITestCase nutzen
Swagger/OpenAPI
drf-spectacular
Production-Ready
Docker, HTTPS, CORS
Nutze ViewSets als Standard!
Nur bei sehr speziellen Anforderungen auf Generic Views oder APIView zurückgreifen.